﻿using GEPL.InterfaceLibrary;
using GEPL.ViewModels;
using IronPython.Hosting;
using Microsoft.Scripting;
using Microsoft.Scripting.Hosting;
using PropertyChanged;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Windows;
using System.Windows.Input;

namespace GEPL.Projects
{
    /// <summary>
    /// Represents an abstract project.
    /// Contains main logic.
    /// </summary>
    [ImplementPropertyChanged]
    [Serializable]
    public abstract class Project : ISerializable
    {
        /// <summary>
        /// List of elements (blocks) of program
        /// </summary>
        public ObservableCollection<Element> Elements { get; private set; }

        public bool IsExecuted { get; set; }

        #region Python engine

        protected ScriptEngine Engine { get; set; }

        protected ScriptScope Scope { get; set; }

        protected ScriptRuntime Runtime { get; set; }

        protected BackgroundWorker backgroundWorker;

        #endregion Python engine

        protected Project()
        {
            Elements = new ObservableCollection<Element>();
            InitializeEngine();
        }

        /// <summary>
        /// Deserialization constructor
        /// </summary>
        protected Project(SerializationInfo info, StreamingContext context)
        {
            Elements = (ObservableCollection<Element>)info.GetValue(nameof(Elements), typeof(ObservableCollection<Element>));
            InitializeEngine();
        }

        #region Public methods

        /// <summary>
        /// Executes the code collected from all Elements
        /// </summary>
        public virtual void Execute()
        {
            Execute(() => { });
        }

        /// <summary>
        /// Executes the code collected from all Elements and invokes additional action.
        /// </summary>
        public virtual void Execute(Action postExecuteAction)
        {
            if (Properties.Settings.Default.Settings_ClearScope)
                ResetVariables();

            var code = GetCodeFromElements();

            using (backgroundWorker = new BackgroundWorker())
            {
                backgroundWorker.DoWork += BackgroundWorker_DoWork;
                backgroundWorker.RunWorkerCompleted += (s, e) =>
                {
                    postExecuteAction();
                    IsExecuted = false;
                    // refresh commands (reevaluate CanExecute functions)
                    CommandManager.InvalidateRequerySuggested();
                };
                IsExecuted = true;
                backgroundWorker.RunWorkerAsync(code);
            }
        }

        /// <summary>
        /// Collects code from all Elements
        /// </summary>
        /// <returns>Code to be executed</returns>
        public string GetCodeFromElements()
        {
            var sb = new StringBuilder();

            int indentLevel = 0;

            foreach (var element in Elements)
            {
                int il = indentLevel;

                // Python uses simple indentation instead of brackets
                // so to implement structures like if statement
                // we have to manage this indentation
                switch (element.ActualElement.ElementType)
                {
                    case ElementType.NormalStatement:
                        break;

                    case ElementType.StartBlock:
                        il = indentLevel++;
                        break;

                    case ElementType.MiddleBlock:
                        il = indentLevel - 1;
                        break;

                    case ElementType.EndBlock:
                        il = --indentLevel;
                        break;
                }

                var script = element.ActualElement.GetScript();
                if (String.IsNullOrEmpty(script))
                    continue;
                var scriptLines = script.Split('\n');

                // for multiline elements, e.g. SetVariable
                // we have to append each line separately,
                // to keep correct indentation
                foreach (var scriptLine in scriptLines)
                {
                    sb.Append(Indent(il));
                    sb.AppendLine(scriptLine);
                }
            }

            return sb.ToString();
        }

        /// <summary>
        /// Used during serialization
        /// </summary>
        public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue(nameof(Elements), Elements, typeof(ObservableCollection<Element>));
        }

        #endregion Public methods

        #region Private methods

        /// <summary>
        /// Initializes the Python engine
        /// </summary>
        private void InitializeEngine()
        {
            Engine = Python.CreateEngine();
            Scope = Engine.CreateScope();
            Runtime = Engine.Runtime;
            Runtime.LoadAssembly(typeof(String).Assembly);
            Runtime.LoadAssembly(typeof(Uri).Assembly);
        }

        private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            try
            {
                var source = Engine.CreateScriptSourceFromString(e.Argument as String, SourceCodeKind.Statements);
                source.Execute(Scope);
            }
            catch (Exception ex) // in case of an exception during code execution, show a message
            {
                var eo = Engine.GetService<ExceptionOperations>();
                var error = eo.FormatException(ex);

                MessageBox.Show(error);
            }
        }

        private void ResetVariables()
        {
            foreach (var variableName in
                Scope.GetVariableNames().Where(variableName => !Properties.Settings.Default.VariablesNotToReset.Contains(variableName)))
            {
                Scope.RemoveVariable(variableName);
            }
        }

        /// <summary>
        /// Returns given number of tab chars
        /// </summary>
        /// <param name="indentLevel">Nr of tab chars to generate</param>
        private static string Indent(int indentLevel)
        {
            var sb = new StringBuilder();

            for (int i = 0; i < indentLevel; i++)
            {
                sb.Append('\t');
            }

            return sb.ToString();
        }

        #endregion Private methods
    }
}